查看原文
其他

C++ 反射 第四章 标准

CPP开发者 2023-07-27

The following article is from CPP编程客 Author 里缪

第一章:C++ 反射:通识

第二章:C++ 反射:探索

第三章:C++ 反射:原生

到了此章,才算是正式进入「C++反射」的主题。

其实从第一章就已然确定了该系列的结构。鉴于「静态反射」还未进入标准,于是有了第二章T0层反射,这是当前项目中可以使用的;再有了第三章T1层反射,这是当前相对完整的反射实践,标准亦会参考Circle。

由于缺少底层机制,因此T0层反射几乎属于玩具级别,而Circle只供语法参考,无法在正式项目中使用。

这篇我们正式进入C++标准反射。

标准反射最早可于C++26/29进入标准,故本章几乎全是比较新的概念。

4.1 C++静态反射与元编程的关系

静态反射加入标准,将会使C++元编程进入一个全新的阶段。

为什么这样说呢?

在C++中,谈论元编程,一般我们是指编译期的编程。

其发展可以分为三个阶段。

第一个阶段,属于模板时期,起于C++98。

模板可以作为编译期计算的工具,对C++影响深远,借其可支持泛型编程,优化代码。缺点是需要对语言具有深入的理解,才能够使用模板来设计与维护代码。

第二个阶段,属于Constant Expressions(常量表达式)时期,起于C++11。

这使得编写编译期代码更加便捷,C++11开始,通过使用constexpr关键字,便可以令某些运算发生于编译期,C++20又添加了consteval与constinit,对编译期计算提供了更多支持。

第三个阶段,则属于静态反射时期,SG7仍在努力中。

大概从2010年,静态反射的研究工作便已启动,最终产生了一个TS版本(N4766),此时属于type-based反射。后来,出于种种原因,SG7转而支持之前就已提出的value-based反射(P0425r0)。

这个阶段将影响深远,极大增强C++元编程的能力,改变大家编程的方式。

本篇文章,就是带领大家进入第三阶段,学习value-based反射的最新成果。

需要提醒大家的是,静态反射本身强调的是反射能力,只有这种能力,根本不够。

因此,伴随静态反射还增加了一些其他提案,比如Expansion Statements用于遍历复数式反射元信息,「源码注入」用于支持更加强大的产生式元编程。

静态反射加上这些相关提案,才真正构成了反射大家族,这才是第三阶段的C++元编程。

4.2 践环境的选择

只是纸上谈兵,社区自然没甚激情,所以SG7提供了一些基于reflection ts(后期value-based版本)的实现,以在社区激起一些浪花。

clang提供有一个支持reflection ts的版本,Compiler Explorer上可以直接使用。

然而这个版本不足以支持本篇内容,因为我们还需要Expansion Statements以及源码注入这些提案的支持,因此选择基于llvm的另一个版本:lock3。

lock3版本现在也没有维护了,但仍是当前最完善的实现版本,编译得半天,大家直接使用Compiler Explorer在线版本就可以,复制链接https://godbolt.org/z/nc34GKvMM 直接进入。

链接当中我已经提前写好了反射所需的全部头文件,这些头文件名称当然也不标准,一个实现一个样。

#include <experimental/meta>
#include <experimental/compiler>

using namespace std::experimental;

int main() {

}

反射属于元编程的第三个阶段,相关特性最终都会包含在<meta>头文件当中。于该头文件的具体内容,后面有一节专门介绍。

完成了前置知识,下面开始正式进入标准反射的内容。

注意!本文讲解的内容基于最新的语法,而编译器的实现仍是较旧或有不符合提案的语法,因此同一个概念,讲解的语法与实际编写代码的语法,存在不一样的形式。各位要记住的是最新的语法,旧语法只是编写代码时不得不向编译器做出的妥协。

4.3 The ^ operator and Splicing

这两个概念在C++反射 第一章:通识中已经介绍过,对应reflectionreification

reflection表示从类型得到「类型元信息」的这个动作,是从具体到抽象、自下而上的结构;而reification表示从「类型元信息」再次得到类型这个动作,是从抽象到具体、自上而下的结构。

这两个是反射通用的概念,在C++反射中reflection对应于“^ operator”,读作lifting operator,表示向上获取类型元对象;reification对应于splicing,语法为"[: refl :]",称为splice construct表示重新具体化对象。

也就是说,"^ operator"是进入反射世界的钥匙,而“[: refl :]”则是回到现实世界的钥匙。

可以再次回到第一章的示例中熟悉一下这些概念。

#include <meta>
template<Enum T>
std::string to_string(T value) {
    template for (constexpr auto e : std::meta::members_of(^T)) {
        if([:e:] == value) {
            return std::string(std::meta::name_of(e));
        }
    }
    return "<unnamed>";
}

细节第一章讲了,不再赘述。

这个例子本身没什么用,主要是为了串联起诸多概念。

不过,这个例子当前编译不过,因为这两个概念的新语法当前没有任何编译器支持。

那么这个例子在lock3下如何编写呢?

代码如下:

1template<typename T>
2requires std::is_enum_v<T>
3consteval const char* enum_to_string(T value) {
4    template for (constexpr auto e : meta::members_of(reflexpr(T))) {
5        if(idexpr(e) == value) {
6            return __concatenate(meta::name_of(e));
7        }
8    }
9    return __concatenate("<unnamed>");
10}
11
12enum class Colors : unsigned char {
13    Red,
14    Green,
15    Blue,
16    Yellow,
17    Black
18};
19
20int main() {
21    constexpr const char* color_name = enum_to_string(Colors::Black);
22    constexpr auto __dummy = __reflect_print(color_name);
23}

为什么反射学起来很乱呢?就是因为概念之间变化太快了,一会叫这个,一会叫那个。

lock3当前实现的lifting operator还是最早使用的占位符:reflexpr()。而splicing的支持都是自己提供的,不像"[: ... :]"语法具有一致性,它提供了好几个操作符来支持不同的情况,idexpr()就是其中之一。

所有的操作都发生于编译期,而C++还不支持constexpr string(虽然可以借助std::string_view),但是lock3提供的__concatenate()用来产生编译期字符串要更加好用。此外,要在编译期输出constexpr string,lock3提供了__reflect_print()

反射结果称为「元对象」(metaobject),也就是反射类型,定义为:

1namespace std::meta {
2    using info = decltype(^void);
3}

这是一个唯一的编译期常量值,所有的反射相关函数,参数都有meta::info。

通过lifting operator就能得到反射类型,再通过meta::members_of()来根据元对象获取类型的所有成员,它的返回值不止一个,若要遍历就得使用expansion statements。

运行上述程序,最终将会在编译期输出枚举值的字符形式,如图。

4.4 标准元编程库

标准元编程库,头文件为<meta>,其中的所有函数称为「元函数」(metafunctions)。

元编程库主要包含两部分内容,第一部分包含元编程的一些通用组件,第二部分包含反射TS中的元函数。

首先来看通用组件部分。

通用组件提供许多针对类型的工具,这些工具大多来自<type_traits>,只是将那些特性加到了value-based反射中来,以支持元对象(即反射类型meta::info)作为参数。

比如remove_const,定义为:

consteval info remove_const(info type) {
  detail::require_type(type);
  return __reflect(detail::query_remove_const, type);
}

那么如何使用呢?

其实和type_triats中一样,只是针对的是元对象,举个例子:

constexpr auto MyInt = meta::remove_const(reflexpr(const int));
typename(MyInt) var = 10;
var = 42;

这里通过lifting operator得到const int的元对象,再通过元函数移除该类型的const修饰,便得到了一个MyInt元对象。通过typename()可以将元对象splicing为一个类型,这代码就相当于int var = 10

补充一下,上述splicing语法最新写法为typename[:MyInt:]

因为<type_traits>大家基本都用过,只是将那些工具引入到反射里面,所以就不浪费篇幅介绍,以后的实际应用中再作解释。

接着来说第二部分,这些工具皆来自反射TS,都是新的元函数。

比如下面的例子,检测类成员权限:

1struct player {
2public:
3    float position_x;
4private:
5    float position_y;
6protected:
7    float position_z;
8};
9
10int main() {
11    constexpr bool pub = meta::is_public(reflexpr(player::position_x));
12    constexpr bool pri = meta::is_private(reflexpr(player::position_x));
13    constexpr bool pro = meta::is_protected(reflexpr(player::position_x));
14}

在反射之前,C++元编程并不具备这样的能力。

上面这种返回bool值的元函数,属于predicates一类,这个类别里面还有许多,列举一些如下:

1std::meta::is_unnamed
2std::meta::is_scoped_enum
3std::meta::is_declraed_constexpr
4std::meta::is_consteval
5std::meta::is_static
6std::meta::is_inline
7std::meta::is_deleted
8std::meta::is_defaulted
9std::meta::is_explicit
10std::meta::is_override
11std::meta::is_pure_virtual
12std::meta::is_class_member
13std::meta::is_local
14std::meta::is_namespace
15std::meta::is_template
16std::meta::is_type
17std::meta::is_incompltete_type
18std::meta::is_closure_type
19std::meta::has_captures
20std::meta::has_default_ref_capture
21std::meta::has_default_copy_capture
22std::meta::is_simple_capture
23std::meta::is_ref_capture
24std::meta::is_copy_capture
25std::meta::is_explicit_capture
26std::meta::is_init_capture
27std::meta::is_function_parameter
28std::meta::is_template_parameter
29std::meta::is_class_template
30std::meta::is_alias
31std::meta::is_alias_template
32std::meta::is_enumberator
33std::meta::is_variable
34std::meta::is_variable_template
35std::meta::is_static_data_member
36std::meta::is_nonstatic_data_member
37std::meta::is_bit_field
38std::meta::is_base_class
39std::meta::is_direct_base_class
40std::meta::is_virtual_base_class
41std::meta::is_function
42std::meta::is_function_template
43std::meta::is_member_function
44std::meta::is_member_function_template
45std::meta::is_static_member_function
46std::meta::is_static_member_function_template
47std::meta::is_nonstatic_member_function
48std::meta::is_nonstatic_member_function_template
49std::meta::is_constructor
50std::meta::is_constructor_template
51std::meta::is_destructor
52std::meta::is_destructor_template
53std::meta::is_lvalue
54std::meta::is_xvalue
55std::meta::is_prvalue
56std::meta::is_glvalue
57std::meta::is_rvalue
58std::meta::has_ellipsis
59std::meta::is_member_function_type
60std::meta::has_default

下面再来看元函数的另一个类别,单数形式。

意思很明显,此类元函数只会返回一个结果,比如前面使用过的meta::name_of()用来获取类型的名称。

这里再给个小例子,如何打印类型名称:

int a = 10;
constexpr auto r = meta::type_of(reflexpr(a));
constexpr auto n = meta::name_of(r);
std::cout << n; // output: int

这个类别也有一些元函数,不过lock3中实现的不多,列举一些如下:

1std::meta::source_location_of
2std::meta::name_of
3std::meta::display_name_of
4std::meta::entity
5std::meta::type_of
6std::meta::parent_of
7std::meta::current_function
8std::meta::current_class_type
9std::meta::byte_offset_of
10std::meta::bit_offset_of
11std::meta::byte_size_of
12std::meta::bit_size_of
13std::meta::this_ref_type

这里列举的很多元函数编译器目前不支持,大家注意一下。

最后来看元函数的另一个类别,复数形式。

顾名思义,就是返回多个结果的元函数。

这里举个打印类成员的例子:

1struct player {
2    static int x;
3    double y;
4private:
5    float z;
6protected:
7    void foo() {}
8};
9
10template<typename T>
11void print_members() {
12    constexpr auto members = meta::members_of(reflexpr(T));
13    template for (constexpr auto m : members) {
14        constexpr auto __dummy = __reflect_pretty_print(m);
15    }
16}
17
18int main() {
19    print_members<player>();
20}

在编译期将会输出:

static int x
double y
float z
void foo() 
{
}

打印所有成员,包括private与protected成员。这里meta::members_of()就是一个返回复数式反射信息的元函数,遍历这种元函数的结果,就需要使用expansion statements

__reflect_pretty_print()是lock3提供的另一个编译期输出工具,参数为元对象,可以直接打印类型的实际声明形式。

meta::members_of()还有第二个参数,可以指定predicates一类的元函数,作为约束条件。比如:

// 获取所有非静态数据成员
meta::members_of(reflexpr(T), meta::is_nonstatic_data_member);
// 获取所有私有成员
meta::members_of(reflexpr(T), meta::is_private);
// 获取所有保护成员
meta::members_of(reflexpr(T), meta::is_protected);

同样,我们也可以获取函数的参数,代码如下:

1void foo(int a, double b, const std::string& c) {
2}
3
4void print_parameters() {
5    constexpr auto parameters = meta::parameters_of(reflexpr(foo));
6    template for (constexpr auto p : parameters) {
7        constexpr auto __dummy = __reflect_pretty_print(p);
8    }
9}
10
11int main() {
12    print_parameters();
13}

输出将为:

int a
double b
const std::string c

以上大概就是对<meta>库的一个整体介绍,下面将进入对于元编程来说,更加重要的一个模块:源码注入。

4.5 源码注入

什么是源码注入?为什么它如此重要呢?

我们使用反射,最主要的目的是使用「产生式元编程」,反射属于基本组件,让我们能够操纵「类型元信息」。而在此之上,需要一些其他特性,来支持使用反射进行产生式元编程。

源码注入,就是伴随反射而来的提案,使得我们可以编写产生代码的代码。

还是以一个简单的例子开始:

1struct X {
2    consteval {
3        for (int num = 0; num < 10; ++num)
4            -> fragment struct {
5                int unqualid("variable_", %{num});
6            };
7    }
8};

此处便使用源码注入,自动生成了以下代码:

1struct X {
2    int variable_0;
3    int variable_1;
4    int variable_2;
5    int variable_3;
6    int variable_4;
7    int variable_5;
8    int variable_6;
9    int variable_7;
10    int variable_8;
11    int variable_9;
12};

我们要进行注入的代码区域,称为metaprogram,语法如下:

consteval {
    ...
};

可以将metaprogram当作是一个不需要参数的consteval函数,编译时会自动执行。

metaprogram中有一些其他操作,比如例子中的for自动产生num,这些操作是不需要注入的,真正需要注入的语句,只要通过「注入语句」的语法"->"指定,就可以将这些代码注入到源码中去。

注入语句跟着的需要注入的代码片段称为fragments,是一个表达式,类型为元对象(meta::info)。

fragments有许多种类,比如namespace fragments, class fragments, enumeration fragments, block fragments,分别表示注入到不同的源码中去。例子中使用的fragment struct,就属于class fragments,也存在fragment class,区别与structclass的区别一样。

注意,class fragments并不是描述一个真正的类,它仅仅是描述类中的成员。也就是说,不用给这个类起名字,起了也会被替换掉,最终被注入的只是这个类的body。class fragments只能注入到类里面。

那么如何在fragments中引入外部的变量呢?

这就需要使用unquote operator了,语法为"%{ ... }",它允许引用局部变量作为fragments中的变量名或类型名。

unqualid operator,可以让我们从字符串组合新的代码。

前面说过,fragments的类型为元对象,那么其实是可以分开定义的,这是再举个fragment enumeration的例子:

constexpr meta::info rgb = fragment enum {
    red, blue, green
};

enum class color {
    consteval -> rgb
};

这里将会把fragments中的值注入到枚举的源码中去,metaprogram只有一行的情况下,就可以省略"{}"。

因此上面源码注入之后的代码为:

enum class color {
    red,
    blue,
    green
};

配合反射,可以实现更多强大的源码注入能力,因为反射的类型也是元对象,所以可以直接注入。

看个简单的例子:

struct A {
    void foo() {}
};

struct X {
    consteval -> reflexpr(A::foo);
};

这样就轻松地将A的成员函数,注入到了X之中。 

同时,也可以在注入的过程中,修改原始定义,比如:

1struct X {
2    consteval {
3        meta::info foo_refl = reflexpr(A::foo);
4        meta::make_constexpr(foo_refl);
5
6        const char* name = meta::name_of(foo_refl);
7        meta::set_new_name(foo_refl, __concatenate("constexpr_", name));
8
9        -> foo_refl;
10    }
11};

这就相当于如下声明:

struct X {
    constexpr void constexpr_foo() {}
};

可以看到,反射配合源码注入,具备强大的产生式元编程能力。

关于源码注入,编译器支持的也不算全,本篇暂时只介绍这么多内容,更多内容以后再写。

以下各节,展示一些不太复杂的使用情境。

4.6 自动生成getters和setters

通过标准反射与源码注入,可以轻松为类成员实现getterssetters函数。

当前有如下类:

1struct book {
2    std::string title;
3    std::string author;
4    int page_count;
5
6    consteval {
7        gen_members(reflexpr(book));
8    }
9};

通过使用fragments,我们想自动生成如下代码:

1struct book {
2    std::string title;
3    std::string author;
4    int page_count;
5
6    std::string get_title() const {
7        return title;
8    }
9
10    void set_title(const std::string& title) {
11        this->title = title;
12    }
13
14    std::string get_author() const {
15        return author;
16    }
17
18    void set_author(const std::string& author) {
19        this->author = author;
20    }
21
22    int get_page_count() const {
23        return page_count;
24    }
25
26    void set_page_count(const int& page_count) {
27        this->page_count = page_count;
28    }
29};

那么首先,定义gen_members()函数,代码如下:

1consteval void gen_members(meta::info cls) {
2    auto members =  meta::members_of(cls, meta::is_nonstatic_data_member);
3    for (meta::info member : members) {
4        gen_member(member);
5    }
6}

通过元函数遍历出类的所有非静态数据成员,再为每个数据成员生成gettersetter函数。

gen_member()函数的定义如下:

1consteval void gen_member(meta::info m) {
2    -> fragment struct {
3        typename(meta::type_of(%{m}))
4        unqualid("get_", meta::name_of(%{m}))() const {
5            return unqualid(meta::name_of(%{m}));
6        }
7    };
8
9    -> fragment struct {
10        void unqualid("set_", meta::name_of(%{m}))(const typename(meta::type_of(%{m}))& unqualid(meta::name_of(%{m}))) {
11            this->unqualid(meta::name_of(%{m})) = unqualid(meta::name_of(%{m}));
12        }
13    };
14}

通过定义两个fragments,利用元编程库中的组件,与源码注入的相关特性,就能够达到自动产生代码的能力。

这个fragments具备可复用性,对所有的类都适用。

这种对当前类生成一些新代码,有一种更推荐的方式,称为Metaclasses,这个概念属于源码注入。

上述例子以这种方式编写,代码如下:

1consteval void gen_members(meta::info cls) {
2    auto members =  meta::members_of(cls, meta::is_nonstatic_data_member);
3    for (meta::info member : members) {
4        ->member;
5
6        gen_member(member);
7    }
8}
9
10struct(gen_members) book {
11    std::string title;
12    std::string author;
13    int page_count;
14};

Metaclasses就是一个metaprogram,不过它强调的是从一个类的原型(prototype)来为该类产生新的代码。

Metaclasses这种方式的语法为"struct(...)",这样就无需在类内编写metaprogram,这种方式要更加安全,编写的代码量也更少。

这里有一点需要注意,编译器在在实例化该定义时,会有一个隐藏的self元对象,还有一个处于匿名空间之中的book类,这个就是原型类。所有声明的成员处于原型类中,self当中没有,因此要使用"->member"将这些成员重新产生出来,否则book类当中找不到声明的那些成员。

此外,其实这个功能的实现,需要另一个特性配合才更好用,那就是「自定义Attributes」,存在相关提案,但是目前没法使用。

4.7 自动生成SQL语句

这个功能是第二章中出现的例子,现在更新为标准C++反射版本。

代码如下:

1consteval const char* to_sql(meta::info type) {
2    if(type == reflexpr(int))
3        return "INTEGER";
4    else if(type == reflexpr(std::string))
5        return "TEXT";
6
7    return "UNKNOWN_TYPE";
8}
9
10consteval const char* create_column(meta::info member) {
11    return __concatenate(meta::name_of(member), 
12        " ", to_sql(meta::type_of(member)));
13}
14
15template<meta::info Class>
16consteval const char* create_table() {
17    const char* table_name = meta::name_of(Class);
18
19    const char* sql = __concatenate("CREATE TABLE ", table_name, "(");
20    bool first_seen = false;
21    for(meta::info member : meta::data_member_range(Class)) {
22        if(first_seen)
23            sql = __concatenate(sql, ", ");
24
25        sql = __concatenate(sql, create_column(member));
26        first_seen = true;
27    }
28    return __concatenate(sql, ");");
29}

代码逻辑第二章讲过,这里不再赘述。

测试代码,将会输出:

1struct person {
2    int age;
3    std::string name;
4};
5
6int main() {
7   std::cout << create_table<reflexpr(person)>();
8}
9
10// 输出:CREATE TABLE person(age INTEGER, name TEXT);

4.8 总结

还有其他一些例子当前编译器编译不了,本文就暂时先举这几个例子。

C++静态反射,核心内容本篇几乎全覆盖到了,重要的是要理解反射的基本语法与标准元编程库,以及源码注入。

静态反射和源码注入,对于产生式元编译来说,是非常强大的工具,能以此构建许多优秀的库或框架。

但是目前仍不完善,还在发展中,语法也没有完全统一,要进标准还得不少时间。

不过了解了这些概念,大家使用其他反射库应该不再是问题。

- EOF -


加主页君微信,不仅C/C++技能+1

主页君日常还会在个人微信分享C/C++开发学习资源技术文章精选,不定期分享一些有意思的活动岗位内推以及如何用技术做业余项目

加个微信,打开一扇窗


推荐阅读  点击标题可跳转

1、C++ 反射:第三章 原生

2、C++ 反射:探索

3、C++ 反射:深入探究 function 实现机制!


关注『CPP开发者』

看精选C++技术文章 . 加C++开发者专属圈子

点赞和在看就是最大的支持❤️

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存